对象

# 对象

# 语法

对象可以通过两种形式定义:声明(文字)形式和构造形式。 对象的文字语法:

  var obj = {};

构造形式:

  var obj = new Object();

构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个键 / 值对,但是在构造形式中你必须逐个添加属性。

# 类型

在 JavaScript 中一共有六种主要类型(术语是“语言类型”)

  • String
  • Boolean
  • Number
  • null
  • undefined
  • object

其中 String、Boolean、Number、null、undefined。 是基本数据类型,本身并不是对象。

null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行typeof null 时会返回字符串 "object" 。 1 实际上, null 本身是基本类型。

JavaScript 中有许多特殊的对象子类型,我们可以称之为复杂基本类型。

  函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。JavaScript 中的函
  数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作
  其他对象一样操作函数(比如当作另一个函数的参数)。
  
  数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要
  稍微复杂一些。

为什么 null 是 对象类型 object

  原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判
  断为 object 类型, null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“ object ”。

# 内置对象

JavaScript 中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java 中的 String 类。 它们实际上只是一些内置函数。这些内置函数可以当作构造函数(由 new 产生的函数调用——参见第 2 章)来使用,从而可以构造一个对应子类型的新对象。

  var strObj = new String('abc')
  console.log(strObj instanceof Object);

  var str = String('abc')
  console.log(str, strObj);

注意:如果使用 new 操作符来调用的话,那么得到的将会是一个对象,如果不使用 new 来调用,那么将会得到一个显式转换成相应数据类型的数据。

在必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。JavaScript 社区中的大多数人都认为能使用文字形式时就不要使用构造形式。

同样的事也会发生在数值字面量上,如果使用类似 42.359.toFixed(2) 的方法,引擎会把42 转换成 new Number(42) 。对于布尔字面量来说也是如此。

null 和 undefined 没有对应的构造形式,它们只有文字形式。相反, Date 只有构造,没有文字形式。

对于 Object 、 Array 、 Function 和 RegExp (正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。 在某些情况下,相比用文字形式创建对象,构造形式可以提供一些额外选项。 由于这两种形式都可以创建对象,所以我们首选更简单的文字形式。 建议只在需要那些额外选项时使用构造形式。

Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建

# 内容

对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。

当我们说“内容”时,似乎在暗示这些值实际上被存储在对象内部,但是这只是它的表现形式。 在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。 存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。

假如我们有一个对象 obj :

  var oj = {
    "a":2
  }

如果要访问 obj 中 a 位置上的值,我们需要使用 .[] 操作符。 .a 语法通常被称为“属性访问”, ["a"] 语法通常被称为“键访问”。

# 可计算属性名

ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名

  var keygen = Symbol('key')
  var obj = {
      [keygen]: 'hello this is key of Symbol'
  }
  console.log(obj[keygen])

# 属性和方法

从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为“方法”似乎有点不妥。

确实,有些函数具有 this 引用,有时候这些 this 确实会指向调用位置的对象引用。 但是这种用法从本质上来说并没有把一个函数变成一个“方法”,因为 this 是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。

最保险的说法可能是,“函数”和“方法”在 JavaScript 中是可以互换的。

ES6 增加了 super 引用,一般来说会被用在 class 中。 super 的行为似乎更有理由把 super 绑定的函数称为“方法”。 但是再说一次,这些只是一些语义(和技术)上的微妙差别,本质是一样的。

# 复制对象

首先,我们应该判断它是浅复制还是深复制。 对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解 析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法

  var newObj = JSON.parse( JSON.stringify( someObj ) );

当然,这种方法需要保证对象是 JSON 安全的,所以只适用于部分情况。

相比深复制,浅复制非常易懂并且问题要少得多,所以 ES6 定义了 Object.assign(..) 方法来实现浅复制。 Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。 它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(owned key,很快会介绍)并把它们复制(使用 = 操作符赋值) 到目标对象,最后返回目标对象。

  var newObj = Object.assign( {}, {a:2},{b:3} );

# 属性描述符

在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。 但是从 ES5 开始,所有的属性都具备了属性描述符。 获取对象属性描述符可以使用 Object.getOwnPropertyDescriptor(obj,"prop") 如下,我们获取了一下 obj 对象的 name 属性的属性描述符:

  var obj = {
   "name":"zhang san"
  };
  console.log(Object.getOwnPropertyDescriptor(obj,"name"));
  /*
    {
      configurable: true,
      enumerable: true,
      value: "zhangsan",
      writable: true
    }
  */

它除了包含属性值之外还包括另外三个特性:

注意:属性描述必须如果只设置其中一个另外两个默认为 false

  • writable 可写
  • enumerable 可枚举
  • configurable 可配置

在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是 configurable )并对特性进行设置。 如:

  var obj = {}
  Object.defineProperty(obj, "name", {
      value: 'zhang san',
      writable: true,
      enumerable: true,
      configurable: true,
  })
  console.log(obj, Object.getOwnPropertyDescriptor(obj, 'name'))

1、Writable 决定是否可以修改属性的值

  var obj = {}
  Object.defineProperty(obj, "name", {
      value: 'zhang san',
      writable: false,
      enumerable: true,
      configurable: true,
  })
  console.log(obj,)
  obj.name = 'lisa'
  console.log(obj);

如你所见,我们对属性值的修改静默失败(silently failed),如果在严格模式下,这种方法会报错。

2、Configurable 决定我们的属性描述符是否可以配置

  var obj = {}
  Object.defineProperty(obj, "name", {
      value: 'zhang san',
      writable: true,
      enumerable: true,
      configurable: false,
  })
  Object.defineProperty(obj, 'name', {
      value: 'wang wu',
      writable: false,
      configurable: false,
      enumerable: false
  })
  console.log(obj)
  obj.name = 'lisa'
  console.log(Object.getOwnPropertyDescriptor(obj, 'name'));

我们发现最后一个 Object.defineProperty() 抛出了一个 TypeError 告诉我们不能重复定义属性描述符 注意:如你所见,把 configurable 修改成false 是单向操作,无法撤销!

但是有一个例外,如果我们只修改 writable 这个特性的话,是不会抛出这个 TypeError 的。但只限于将 writable 由 true 改为 false。

  var obj = {}
  Object.defineProperty(obj, "name", {
      value: 'zhang san',
      writable: true,
      enumerable: true,
      configurable: false,
  })
  Object.defineProperty(obj, 'name', {    // 正常修改
      writable: false,
  })
  Object.defineProperty(obj, 'name', {    // TypeError 
      writable: true,
  })

除了不能修改配置之外,还会影响 delete 删除。因为属性是不可配置。

  var obj = {}
  Object.defineProperty(obj, "name", {
      value: 'zhang san',
      writable: true,
      enumerable: true,
      configurable: false,
  })

  console.log(obj)
  delete obj.name
  console.log(obj);

delete 只用来直接删除对象的(可删除)属性。如果对象的某个属性是某个对象 / 函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象 / 函 数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具(就像 C/C++ 中那样),它就是一个删除对象属性的操作,仅此而已。

3、Enumerable 决定属性是否可枚举 从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in 循环。 如果把 enumerable 设置成 false ,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。

  var obj = {}
  Object.defineProperty(obj, "name", {
      value: 'zhang san',
      writable: true,
      enumerable: true,
      configurable: true,
  })
  Object.defineProperty(obj, 'age', {
      value: 18,
      writable: true,
      configurable: true,
      enumerable: false
  })

  for (const prop in obj) {
      console.log(prop, 'enumerable') // name
  }

  console.log(Object.keys(obj)) // ['name']
  console.log(obj); // {name: "zhang san", age: 18}

# 不变性

所有的方法创建的都是浅不变形,也就是说,它们只会影响目标对象和它的直接属性。 如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的

在 JavaScript 程序中很少需要深不可变性。 有些特殊情况可能需要这样做,但是根据通用的设计模式,如果你发现需要密封或者冻结所有的对象,那 你或许应当退一步,重新思考一下程序的设计,让它能更好地应对对象值的改变。

1、对象常量 结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除)

  var obj = {}
  Object.defineProperty(obj, 'name', {
      writable: false,
      value: 10,
      configurable: false,
      enumerable: true
  })

2、禁止扩展

  var obj = {}
  Object.preventExtensions(obj)
  obj.a = 1
  obj.b = 2
  console.log(obj)

在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。

3、密封 Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false 。 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

4、冻结 Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为 writable:false ,这样就无法修改它们的值。

这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。

你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..) ,然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..) 。但是一定要小心,因 为这样做有可能会在无意中冻结其他(共享)对象。

# [[Get]]

属性访问在实现时有一个微妙却非常重要的细节

  var myObject = {
  a: 2
  };
  myObject.a; // 2

在语言规范中, myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作(有点像函数调用: [Get] )。 对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。

然而,如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要的行为。 -遍历可能存在的 [[Prototype]] 链,也就是原型链

如果无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined

  var myObject = {
  a: undefined
  };
  myObject.a; // undefined
  myObject.b; // undefined

从返回值的角度来说,这两个引用没有区别——它们都返回了 undefined 。然而,尽管乍 看之下没什么区别,实际上底层的 [[Get]] 操作对 myObject.b 进行了更复杂的处理。

# [[Put]]

既然有可以获取属性值的 [[Get]] 操作,就一定有对应的 [[Put]] 操作。

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。

如果已经存在这个属性, [[Put]] 算法大致会检查下面这些内容。

    1. 属性是否是访问描述符(参见 3.3.9 节)?如果是并且存在 setter 就调用 setter。
    1. 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
    1. 如果都不是,将该值设置为属性的值。

# Getter 和 Setter

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。

在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。 getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。

当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。 对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get (还有 configurable 和 enumerable )特性。

  var obj = {
      get a() {
          return Math.random() * 10
      }
  }

  Object.defineProperty(obj, 'b', {
      get() {
          return Math.floor(this.a)
      },
      enumerable: true
  })

  console.log(obj);
  console.log(obj.a);
  console.log(obj.b);

不管是对象文字语法中的 get a() { .. } ,还是 defineProperty(..) 中的显式定义,二者 都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值.

为了让属性更合理,还应当定义 setter,和你期望的一样,setter 会覆盖单个属性默认的 [[Put]] (也被称为赋值)操作。 通常来说 getter 和 setter 是成对出现的(只定义一个的话通常会产生意料之外的行为)

**“数据描述符”和“存取描述符”只能存在一个。

# 存在性

myObject.a 的属性访问返回值可能是 undefined ,但是这个值有可能是属性中存储的 undefined ,也可能是因为属性不存在所以返回 undefined 。 那么如何区分这两种情况呢?

我们可以在不访问属性值的情况下判断对象中是否存在这个属性

  • in 操作符:in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中
      var obj = {
          a: 1,
          b: 3
      }
    
      console.log('a' in obj); // true
      console.log('c' in obj); // false
      console.log('b' in obj); // true
    
  • hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
      var obj = {
          a: 1,
          b: 3
      }
    
      console.log(obj.hasOwnProperty('a')); // true
      console.log(obj.hasOwnProperty('c')); // false
      console.log(obj.hasOwnProperty('b')); // true
    
    看起来 in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。 比如数组 [1,2,3] 但是如果你检查 3 的话是不存在的,因为数组的键是从0开始依次排列的。

可枚举

  var obj = {}

  Object.defineProperty(obj, 'a', {
      enumerable: true,
      value: 1
  })

  Object.defineProperty(obj, 'b', {
      enumerable: false,
      value: 2
  })

  console.log('a' in obj); // true
  console.log('b' in obj); // true

  console.log(obj.hasOwnProperty('a')); // true
  console.log(obj.hasOwnProperty('b')); // true

  for (const p in obj) {
      console.log(p, 'enumer-key');
  }
  // a enumer-key

我们发现我们定义的两个属性都在对象 obj 中,只不过 a 是可枚举的, b 是不可枚举的 当我们访问 obj.b 的时候确实存在访问值,但是却不会出现在 for ··· in 循环中 原因是可枚举就相当于“可以出现在对象属性的遍历中”

在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。 最好只在对象上应用 for..in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引。

也可以通过另一种方式来区分属性是否可枚举

  var obj = {}

  Object.defineProperty(obj, 'a', {
      enumerable: true,
      value: 1
  })

  Object.defineProperty(obj, 'b', {
      enumerable: false,
      value: 2
  })


  console.log(obj.propertyIsEnumerable('a'));  // true
  console.log(obj.propertyIsEnumerable('b'));  // false

  console.log(Object.keys(obj));  // ['a']
  console.log(Object.getOwnPropertyNames(obj)); // ['a','b']

# 遍历

for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。但是如何遍历属性的值呢 对于数值索引的数组来说,可以使用标准的 for 循环来遍历值 这实际上并不是在遍历值,而是遍历下标来指向值

ES6 增加了一种用来遍历数组的 for..of 循环语法(如果对象本身定义了迭代器的话也可以遍历对象) for..of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来遍历所有返回值。

数组有内置的 @@iterator ,因此 for..of 可以直接应用在数组上。我们使用内置的 @@iterator 来手动遍历数组

  var arr = [1, 2, 3, 4, 5]

  for (const val of arr) {
      console.log(val);
  }

那么如何自定义迭代器来遍历对象的属性呢?

  var obj = {
      a: 1,
      b: 2,
      c: 3,
      d: 4
  }

  Object.defineProperty(obj, Symbol.iterator, {
      writable: true,
      configurable: true,
      enumerable: true,
      value: function () {
          let key_arr = Object.keys(this);
          let index = 0;
          let self = this;
          return {
              next: function () {
                  return {
                      value: self[key_arr[index++]],
                      done: index > key_arr.length
                  }
              }
          }
      }
  })

  for (const val of obj) {
      console.log(val);
  }

我们使用 Object.defineProperty(..) 定义了我们自己的 @@iterator (主要是为了让它不可枚举),不过注意, 我们把符号当作可计算属性名(本章之前有介绍)。此外,也可以直接在定义对象时进行声明, 比如 var myObject = { a:2, b:3, [Symbol.iterator]: function() { /* .. */ } } 。

注意:一定要给迭代器一个出口,不然的话会无限循环

# 小结

对象就是键 / 值对的集合。可以通过 .propName 或者 ["propName"] 语法来获取属性值。访问属性时, 引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]] ),[[Get]] 操作会检查对象本身是否包含这个属性, 如果没找到的话还会查找 [[Prototype]] 链。

属性的特性可以通过属性描述符来控制,比如 writable 和 configurable 。此外,可以使用 Object.preventExtensions(..) 、 Object.seal(..) 和 Object.freeze(..) 来设置对象(及其属性)的不可变性级别。

属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中。

你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值, for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。